/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule update
 */

/* global hasOwnProperty:true */

'use strict'

var _assign = require('object-assign')

var keyOf = require('fbjs/lib/keyOf')
var invariant = require('fbjs/lib/invariant')
var hasOwnProperty = {}.hasOwnProperty

function shallowCopy(x) {
  if (Array.isArray(x)) {
    return x.concat()
  } else if (x && typeof x === 'object') {
    return _assign(new x.constructor(), x)
  } else {
    return x
  }
}

var COMMAND_PUSH = keyOf({ $push: null })
var COMMAND_UNSHIFT = keyOf({ $unshift: null })
var COMMAND_SPLICE = keyOf({ $splice: null })
var COMMAND_SET = keyOf({ $set: null })
var COMMAND_MERGE = keyOf({ $merge: null })
var COMMAND_APPLY = keyOf({ $apply: null })

var ALL_COMMANDS_LIST = [
  COMMAND_PUSH,
  COMMAND_UNSHIFT,
  COMMAND_SPLICE,
  COMMAND_SET,
  COMMAND_MERGE,
  COMMAND_APPLY
]

var ALL_COMMANDS_SET = {}

ALL_COMMANDS_LIST.forEach(function (command) {
  ALL_COMMANDS_SET[command] = true
})

function invariantArrayCase(value, spec, command) {
  !Array.isArray(value)
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'update(): expected target of %s to be an array; got %s.',
          command,
          value
        )
      : invariant(false)
    : void 0
  var specValue = spec[command]
  !Array.isArray(specValue)
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'update(): expected spec of %s to be an array; got %s. ' +
            'Did you forget to wrap your parameter in an array?',
          command,
          specValue
        )
      : invariant(false)
    : void 0
}

function update(value, spec) {
  !(typeof spec === 'object')
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'update(): You provided a key path to update() that did not contain one ' +
            'of %s. Did you forget to include {%s: ...}?',
          ALL_COMMANDS_LIST.join(', '),
          COMMAND_SET
        )
      : invariant(false)
    : void 0

  if (hasOwnProperty.call(spec, COMMAND_SET)) {
    !(Object.keys(spec).length === 1)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'Cannot have more than one key in an object with %s',
            COMMAND_SET
          )
        : invariant(false)
      : void 0

    return spec[COMMAND_SET]
  }

  var nextValue = shallowCopy(value)

  if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
    var mergeObj = spec[COMMAND_MERGE]
    !(mergeObj && typeof mergeObj === 'object')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            "update(): %s expects a spec of type 'object'; got %s",
            COMMAND_MERGE,
            mergeObj
          )
        : invariant(false)
      : void 0
    !(nextValue && typeof nextValue === 'object')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            "update(): %s expects a target of type 'object'; got %s",
            COMMAND_MERGE,
            nextValue
          )
        : invariant(false)
      : void 0
    _assign(nextValue, spec[COMMAND_MERGE])
  }

  if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
    invariantArrayCase(value, spec, COMMAND_PUSH)
    spec[COMMAND_PUSH].forEach(function (item) {
      nextValue.push(item)
    })
  }

  if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
    invariantArrayCase(value, spec, COMMAND_UNSHIFT)
    spec[COMMAND_UNSHIFT].forEach(function (item) {
      nextValue.unshift(item)
    })
  }

  if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
    !Array.isArray(value)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'Expected %s target to be an array; got %s',
            COMMAND_SPLICE,
            value
          )
        : invariant(false)
      : void 0
    !Array.isArray(spec[COMMAND_SPLICE])
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'update(): expected spec of %s to be an array of arrays; got %s. ' +
              'Did you forget to wrap your parameters in an array?',
            COMMAND_SPLICE,
            spec[COMMAND_SPLICE]
          )
        : invariant(false)
      : void 0
    spec[COMMAND_SPLICE].forEach(function (args) {
      !Array.isArray(args)
        ? process.env.NODE_ENV !== 'production'
          ? invariant(
              false,
              'update(): expected spec of %s to be an array of arrays; got %s. ' +
                'Did you forget to wrap your parameters in an array?',
              COMMAND_SPLICE,
              spec[COMMAND_SPLICE]
            )
          : invariant(false)
        : void 0
      nextValue.splice.apply(nextValue, args)
    })
  }

  if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
    !(typeof spec[COMMAND_APPLY] === 'function')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'update(): expected spec of %s to be a function; got %s.',
            COMMAND_APPLY,
            spec[COMMAND_APPLY]
          )
        : invariant(false)
      : void 0
    nextValue = spec[COMMAND_APPLY](nextValue)
  }

  for (var k in spec) {
    if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {
      nextValue[k] = update(value[k], spec[k])
    }
  }

  return nextValue
}

module.exports = update
